
Le but de ce notebook est d'optimiser la DataLoader afin de ne pas ralentir la boucle d'apprentissage. L'étude de la performance des solutions optimisées se fera en visualisant les traces du profiler :
Les cellules dans ce notebook ne sont pas prévues pour être modifiées, sauf rares exceptions indiquées dans les commentaires. Les TP se feront en modifiant le code dlojz.py.
Les directives de modification seront marquées par l'étiquette TODO : dans le notebook suivant.
Les solutions sont présentes dans le répertoire solutions.
Notebook rédigé par l'équipe assistance IA de l'IDRIS, juin 2023
Un module PyTorch doit avoir été chargé pour le bon fonctionnement de ce Notebook. Nécessairement, le module pytorch-gpu/py3/1.11.0 :
!module list
Currently Loaded Modulefiles: 1) cuda/11.2 5) openmpi/4.1.1-cuda 9) sparsehash/2.0.3 2) nccl/2.9.6-1-cuda 6) intel-mkl/2020.4 10) libjpeg-turbo/2.1.3 3) cudnn/8.1.1.33-cuda 7) magma/2.5.4-cuda 11) pytorch-gpu/py3/1.11.0 4) gcc/8.5.0(8.3.1:8.4.1) 8) sox/14.4.2 >
Les fonctions python de gestion de queue SLURM dévelopées par l'IDRIS et les fonctions dédiées à la formation DLO-JZ sont à importer.
Le module d'environnement pour les jobs et la taille des images sont fixés pour ce notebook.
TODO : choisir un pseudonyme (maximum 5 caractères) pour vous différencier dans la queue SLURM et dans les outils collaboratifs pendant la formation et la compétition.
from idr_pytools import display_slurm_queue, gpu_jobs_submitter, search_log
from dlojz_tools import controle_technique, compare, GPU_underthehood, plot_accuracy, lrfind_plot, imagenet_starter, turbo_profiler
MODULE = 'pytorch-gpu/py3/1.11.0'
account = 'for@v100'
name = 'pseudo' ## Pseudonyme à choisir
Cette partie permet d'afficher et de gérer la queue SLURM.
Pour afficher toute la queue utilisateur :
display_slurm_queue(name)
Done!
Remarque: Cette fonction utilisée plusieurs fois dans ce notebook permet d'afficher la queue de manière dynamique, rafraichie toutes les 5 secondes. Cependant elle ne s'arrête que lorsque la queue est vide. Si vous désirez reprendre la main sur le notebook, il vous suffira d'arrêter manuellement la cellule avec le bouton stop. Cela a bien sûr aucun impact sur le scheduler SLURM. Les jobs ne seront pas arrêtés.
Si vous voulez annuler un job dans votre queue, décommenter la ligne suivante et remplacer le numéro du job.
#!scancel 2088207
Cette partie debug permet d'afficher les fichiers de sortie et les fichiers d'erreur du job.
Il est nécessaire dans la cellule suivante d'indiquer le jobid correspondant sous le format donné.
*Remarque* : dans ce notebook, lorsque vous soumettrez un job, vous recevrez en retour le numéro du job dans le format suivant : jobid = ['123456']. La cellule ci-dessous peut ainsi être facilement actualisée.
#jobid = ['2088207']
Fichier de sortie :
%cat {search_log(contains=jobid[0])[0]}
/bin/bash: -c: line 0: syntax error near unexpected token `('
/bin/bash: -c: line 0: `cat {search_log(contains=jobid[0])[0]}'
Fichier d'erreur :
%cat {search_log(contains=jobid[0], with_err=True)['stderr'][0]}
/bin/bash: -c: line 0: syntax error near unexpected token `('
/bin/bash: -c: line 0: `cat {search_log(contains=jobid[0], with_err=True)['stderr'][0]}'
Pour le debug ou pour comparer son code avec les solutions mises à disposition, la fonction suivante permet d'afficher une page html contenant un différentiel de fichiers texte.
s1 = "dlojz.py"
s2 = "./solutions/dlojz3_1.py"
compare(s1, s2)
Voir le résultat du différentiel de fichiers sur la page suivante (attention au spoil !) :
On fixe le batch size et la taille d'image pour ce TP.
bs_optim = 512
image_size = 176
TODO : Comparer votre script dlojz.py avec ce qu'il devrait être actuellement. Si il y a des divergences, veuillez les corriger (par exemple en copiant-collant la solution).
s1 = "dlojz.py"
s2 = "./solutions/dlojz3_1.py"
compare(s1, s2)
Voir le résultat du différentiel de fichiers sur la page suivante :
# copier/coller la solution si nécessaire
#!cp solutions/dlojz3_0.py dlojz.py
Le but de ce TP est d'utiliser un IterableDataset sur des données d'entrée au format WebDataset et de le comparer avec le Dataset Map-style de torchvision précédemment vu.
TODO : dans le script dlojz.py :
import webdataset as wds
train_dataset, du train_loader et du train_sampler par l'implémentation suivante.train_dataset = (
wds.WebDataset(os.environ['ALL_CCFRSCRATCH']+'/imagenet/webdataset/imagenet_train-{000000..000127}.tar', shardshuffle=True, nodesplitter=wds.split_by_node)
.shuffle(1000)
.decode("torchrgb")
.to_tuple('input.pyd', 'output.pyd')
.map_tuple(transform, lambda x: x)
.batched(mini_batch_size)
)
dataset_size = 1281167
number_of_batches = dataset_size // global_batch_size
train_loader = wds.WebLoader(train_dataset,
batch_size=None,
num_workers=args.num_workers,
persistent_workers=args.persistent_workers,
pin_memory=args.pin_memory,
prefetch_factor=args.prefetch_factor,
drop_last=args.drop_last)
train_loader = train_loader.slice(number_of_batches)
train_loader.length = number_of_batches
train_sampler (la distribution des batches sur les différents workers se fait avec le paramètre nodesplitter=wds.split_by_node), effacer ou commenter la ligne suivante :#train_sampler.set_epoch(epoch)
train_loader.length = number_of_batches. Modifier la déclaration de la variable N_batch en conséquence :N_batch = train_loader.length
TODO : lancer l'exécution sur 50 itérations (--test-nsteps 50) sans profiling pour passer un contrôle technique qui servira de référence. Cette exécution va prendre quelques minutes, vous pouvez passer à la suite du TP sans attendre la fin de l'exécution.
Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.
Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test --test-nsteps 50'
command += f' --num-workers 0 --no-persistent-workers --no-pin-memory --no-non-blocking --prefetch-factor 2'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task Submitted batch job 248210 jobid = ['248210']
display_slurm_queue(name)
JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON)
248210 gpu_p13 pseudo cfor132 R 3:08 1 r6i3n0
Done!
#jobid = ['1587014']
controle_technique(jobid)
Train throughput: 268.87 images/second GPU throughput: 1783.88 images/second epoch time: 4764.49 seconds training time estimation for 90 epochs (with validations): 128.44 hours ----------- training step time average (fwd/bkwd on GPU): 0.287015 sec (39.1%/60.6%) +/- 0.001335 loading step time average (CPU to GPU): 1.617259 sec +/- 0.087032 ----------- ELIGIBLE to run 11 epochs
turbo_profiler(jobid)
>>> Turbo Profiler >>> Training complete in 141.384106 s
TODO : étudier les traces du cas sous-optimisé "num_workers=0" afin de mesurer l'accélération brute de ce type de Dataset.
Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.
Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test --test-nsteps 15 --prof'
command += f' --num-workers 0 --no-persistent-workers --no-pin-memory --no-non-blocking --prefetch-factor 2'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task Submitted batch job 248218 jobid = ['248218']
Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'eviter de relancer un job par erreur.
display_slurm_queue(name)
JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON)
248218 gpu_p13 pseudo cfor132 R 1:59 1 r6i3n0
Done!
#jobid = ['1587676']
TODO : vérifier qu'une trace a bien été générée dans le répertoire profiler/<name>_<jobid>_bs512_is176/ sous la forme d'un fichier .json:
!ls profiler/{name}_{jobid[0]}*
r6i3n0_632542.1687733989173.pt.trace.json
TODO : visualiser cette trace grâce à l'application TensorBoard (retrouver la procédure) et comparer les traces obtenues avec le dataset torchvision et le dataset webdataset.
IMPORTANT : une fois le TP terminé, penser à quitter l'instance JupyterHub pour libérer le GPU ( > Hub Control Panel > Cancel ).
Ensuite, l'objectif de ce TP est de réduire le temps passé sur CPU par le DataLoader WebDataset.
Les différentes optimisations proposées par le DataLoader sont accessibles dans le script dlojz.py via les arguments :
--num-workers <num_workers> (défaut à 10)--persistent-workers (défaut) ou --no-persistent-workers--pin-memory (défaut) ou --no-pin-memory--non-blocking (défaut) ou --no-non-blocking--prefetch-factor <prefetch_factor> (défaut à 3)--drop-last ou --no-drop-last (défaut)TODO : faire varier ces différents paramètres et observer leurs effets grâce au profiler turbo_profiler
Remarque : pour cette étude, on ne lance les exécutions que sur 15 itérations (--test-nsteps 15) pour avancer plus rapidement.
Les différents essais seront stockés dans une DataFrame dataloader_trials :
import pandas as pd
dataloader_trials = pd.DataFrame({"jobid":pd.Series([],dtype=str),
"num_workers":pd.Series([],dtype=int),
"persistent_workers":pd.Series([],dtype=str),
"pin_memory":pd.Series([],dtype=str),
"non_blocking":pd.Series([],dtype=str),
"prefetch_factor":pd.Series([],dtype=int),
"drop_last":pd.Series([],dtype=str),
"loading_time":pd.Series([],dtype=float),
"training_time":pd.Series([],dtype=float)})
Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.
Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test --test-nsteps 15'
# paramètres d'entrée correspondant aux optimisations du DataLoader
command += ' --num-workers 8'
command += ' --persistent-workers'
command += ' --pin-memory'
command += ' --non-blocking'
command += ' --prefetch-factor 2'
command += ' --no-drop-last'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task Submitted batch job 248223 jobid = ['248223']
display_slurm_queue(name)
JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON)
248223 gpu_p13 pseudo cfor132 R 0:29 1 r6i3n0
Done!
jobid = ['248223']
# call turbo_profiler
dataloader_trial = turbo_profiler(jobid,dataloader_info=True)
# store result in "dataloader_trials" DataFrame
dataloader_trials = pd.concat([dataloader_trials,dataloader_trial], ignore_index=True)
>>> Turbo Profiler >>> Training complete in 19.322983 s
# afficher le tableau récapitulatif, trier par ordre croissant du LOADING_TIME
dataloader_trials.sort_values("loading_time")
| jobid | num_workers | persistent_workers | pin_memory | non_blocking | prefetch_factor | drop_last | loading_time | training_time | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 248223 | 8 | True | True | True | 2 | False | 0.000474 | 19.322983 |
# afficher le tableau récapitulatif, trier par ordre croissant du TRAINING_TIME
dataloader_trials.sort_values("training_time")
| jobid | num_workers | persistent_workers | pin_memory | non_blocking | prefetch_factor | drop_last | loading_time | training_time | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 248223 | 8 | True | True | True | 2 | False | 0.000474 | 19.322983 |
TODO : après avoir choisi un lot de paramètres optimal, relancer le job en réactivant le profiler PyTorch (argument d'entrée --prof) afin de visualiser les traces sous TensorBoard.
Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.
Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test --prof --test-nsteps 15'
# définir ici les paramètres optimaux
command += ' --num-workers 8'
command += ' --persistent-workers'
command += ' --pin-memory'
command += ' --non-blocking'
command += ' --prefetch-factor 2'
command += ' --no-drop-last'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task Submitted batch job 248226 jobid = ['248226']
Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.
display_slurm_queue(name)
JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON)
248226 gpu_p13 pseudo cfor132 R 0:37 1 r6i3n0
Done!
TODO : vérifier qu'une trace a bien été générée dans le répertoire profiler/<name>_<jobid>_bs512_is176/ sous la forme d'un fichier .json:
!ls profiler/{name}_{jobid[0]}*
r6i3n0_633725.1687734182850.pt.trace.json
TODO : visualiser cette trace grâce à l'application TensorBoard (retrouver la procédure).
IMPORTANT : une fois le TP terminé, penser à quitter l'instance JupyterHub pour libérer le GPU ( > Hub Control Panel > Cancel ).
TODO : lancer l'exécution sur 50 itérations (--test-nsteps 50) sans profiling pour passer un nouveau contrôle technique, à comparer avec celui de référence.
Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.
Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test --test-nsteps 50'
# définir ici les paramètres optimaux
command += ' --num-workers 8'
command += ' --persistent-workers'
command += ' --pin-memory'
command += ' --non-blocking'
command += ' --prefetch-factor 2'
command += ' --no-drop-last'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task Submitted batch job 248228 jobid = ['248228']
display_slurm_queue(name)
JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON)
248228 gpu_p13 pseudo cfor132 CG 0:43 1 r6i3n0
Done!
#jobid = ['1587014']
controle_technique(jobid)
Train throughput: 1702.20 images/second GPU throughput: 1703.77 images/second epoch time: 752.57 seconds training time estimation for 90 epochs (with validations): 20.53 hours ----------- training step time average (fwd/bkwd on GPU): 0.300510 sec (41.5%/58.3%) +/- 0.001797 loading step time average (CPU to GPU): 0.000278 sec +/- 0.000034 ----------- ELIGIBLE to run 41 epochs
turbo_profiler(jobid)
>>> Turbo Profiler >>> Training complete in 29.88395 s

Il faut repartir d'un scriptdlojz.py propre :
# copier/coller la solution si nécessaire
!cp solutions/dlojz3_0.py dlojz.py
import os
import torchvision
import torchvision.transforms as transforms
import torchvision.models as models
import torch
import numpy as np
import matplotlib.pyplot as plt
transform = transforms.Compose([
transforms.RandomResizedCrop(176), # Random resize - Data Augmentation
transforms.RandomHorizontalFlip(), # Horizontal Flip - Data Augmentation
transforms.RandAugment(5, 9), # Random Augmentation 2: n operations, 9 : magnitude
transforms.ToTensor() # convert the PIL Image to a tensor
])
train_dataset = torchvision.datasets.ImageNet(root=os.environ['ALL_CCFRSCRATCH']+'/imagenet',
transform=transform)
train_dataset
Dataset ImageNet
Number of datapoints: 1281167
Root location: /gpfsscratch/idris/for/commun/imagenet
Split: train
StandardTransform
Transform: Compose(
RandomResizedCrop(size=(176, 176), scale=(0.08, 1.0), ratio=(0.75, 1.3333), interpolation=bilinear)
RandomHorizontalFlip(p=0.5)
RandAugment(num_ops=5, magnitude=9, num_magnitude_bins=31, interpolation=InterpolationMode.NEAREST, fill=None)
ToTensor()
)
%%time
train_loader = torch.utils.data.DataLoader(dataset=train_dataset,
batch_size=4,
shuffle=True)
batch = next(iter(train_loader))
print('X train batch, shape: {}, data type: {}, Memory usage: {} bytes'
.format(batch[0].shape, batch[0].dtype, batch[0].element_size()*batch[0].nelement()))
print('Y train batch, shape: {}, data type: {}, Memory usage: {} bytes'
.format(batch[1].shape, batch[1].dtype, batch[1].element_size()*batch[1].nelement()))
for i in range(4):
img = batch[0][i].numpy().transpose((1,2,0))
plt.imshow(img)
plt.axis('off')
plt.show()
X train batch, shape: torch.Size([4, 3, 176, 176]), data type: torch.float32, Memory usage: 1486848 bytes Y train batch, shape: torch.Size([4]), data type: torch.int64, Memory usage: 32 bytes
CPU times: user 2.15 s, sys: 389 ms, total: 2.54 s Wall time: 2.69 s
TODO : dans le script dlojz.py :
RandAugmentdans la liste des transformations pour la Data Augmentationtransform = transforms.Compose([
transforms.RandomResizedCrop(args.image_size), # Random resize - Data Augmentation
transforms.RandomHorizontalFlip(), # Horizontal Flip - Data Augmentation
transforms.RandAugment(2, 9), # Random Augmentation 2:n operations, 9:magnitude
transforms.ToTensor(), # convert the PIL Image to a tensor
transforms.Normalize(mean=(0.485, 0.456, 0.406),
std=(0.229, 0.224, 0.225))
])
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test'
command
'dlojz.py -b 512 --image-size 176 --test'
Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.
Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task Submitted batch job 248262 jobid = ['248262']
Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.
Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.
#jobid = ['1588551']
display_slurm_queue(name)
JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON)
248262 gpu_p13 pseudo cfor132 R 0:55 1 r6i3n0
Done!
controle_technique(jobid)
Train throughput: 1701.43 images/second GPU throughput: 1704.01 images/second epoch time: 753.21 seconds training time estimation for 90 epochs (with validations): 21.16 hours ----------- training step time average (fwd/bkwd on GPU): 0.300468 sec (43.7%/62.8%) +/- 0.058110 loading step time average (CPU to GPU): 0.000455 sec +/- 0.000671 ----------- ELIGIBLE to run 41 epochs
turbo_profiler(jobid)
>>> Turbo Profiler >>> Training complete in 34.876351 s

Le but de ce TP est d'ajouter la transformation Mixup dans la liste des transformations pour la Data Augmentation et de mesurer grâce au profiler ce que cela implique pour le DataLoader.
La transformation MixUp n'est pas disponible dans torchvision, le script est disponible dans le répertoire mixup/. On notera que cette transformation impacte à la fois l'image et le label.
On choisira, comme cela est fait habituellement, de mixer 2 images présentes dans le batch généré par le DataLoader. Donc cette transformation sera faite dans la boucle d'apprentissage après génération du batch et après toutes autres transformations liées à la Data Augmentation.
import os
import torchvision
import torchvision.transforms as transforms
import torchvision.models as models
import torch
import numpy as np
import matplotlib.pyplot as plt
transform = transforms.Compose([
transforms.RandomResizedCrop(176), # Random resize - Data Augmentation
transforms.RandomHorizontalFlip(), # Horizontal Flip - Data Augmentation
transforms.ToTensor() # convert the PIL Image to a tensor
])
train_dataset = torchvision.datasets.ImageNet(root=os.environ['ALL_CCFRSCRATCH']+'/imagenet',
transform=transform)
train_dataset
Dataset ImageNet
Number of datapoints: 1281167
Root location: /gpfsscratch/idris/for/commun/imagenet
Split: train
StandardTransform
Transform: Compose(
RandomResizedCrop(size=(176, 176), scale=(0.08, 1.0), ratio=(0.75, 1.3333), interpolation=bilinear)
RandomHorizontalFlip(p=0.5)
ToTensor()
)
from mixup.mixup import mixup_data
%%time
train_loader = torch.utils.data.DataLoader(dataset=train_dataset,
batch_size=16,
shuffle=True)
batch = next(iter(train_loader))
print('X train batch, shape: {}, data type: {}, Memory usage: {} bytes'
.format(batch[0].shape, batch[0].dtype, batch[0].element_size()*batch[0].nelement()))
print('Y train batch, shape: {}, data type: {}, Memory usage: {} bytes'
.format(batch[1].shape, batch[1].dtype, batch[1].element_size()*batch[1].nelement()))
imgs, targets = batch
imgs, targets = mixup_data(imgs, targets, num_classes=1000, alpha=2) ## Transformation Mixup
for i in range(4):
img = imgs[i].numpy().transpose((1,2,0))
plt.imshow(img)
plt.axis('off')
plt.show()
print(f'target : {torch.max(targets, dim=1)[1][i]}, lambda : {torch.max(targets, dim=1)[0][i]}')
X train batch, shape: torch.Size([16, 3, 176, 176]), data type: torch.float32, Memory usage: 5947392 bytes Y train batch, shape: torch.Size([16]), data type: torch.int64, Memory usage: 128 bytes
target : 714, lambda : 0.5794603824615479
target : 792, lambda : 0.5220049619674683
target : 866, lambda : 0.7600038647651672
target : 387, lambda : 0.7419617176055908 CPU times: user 6.82 s, sys: 175 ms, total: 6.99 s Wall time: 7.37 s
Paramètre alpha pour la beta distribution
Dans le script mixup.py, la variable lambda (lam) correspond à la proportion de la première image par rapport à la deuxième image. Elle est choisie aléatoirement suivant une distribution bêta définie sur [0, 1].
Le paramètre alpha agit sur la forme de la distribution bêta. alpha = 1 correspond à une distribution uniforme, alpha < 1 favorise un tirage au sort de valeurs proches des bornes 0. ou 1., et alpha > 1 favorise un tirage au sort de valeurs proches du centre 0.5.
for alpha in [0.5, 1., 2.]:
plt.hist(np.random.beta(alpha, alpha, 1000000), bins=50, density=True, histtype='step')
plt.title(f'alpha={alpha}')
plt.show()
TODO : dans le script dlojz.py :
Mixupfrom mixup.mixup import mixup_data
MixUp dans la boucle d'apprentissage avant d'envoyer le batch d'images et de labels au GPU.# distribution of images and labels to all GPUs
images, labels = mixup_data(images, labels, num_classes=1000, alpha=2.)
images = images.to(gpu, non_blocking=True)
labels = labels.to(gpu, non_blocking=True)
# Metric mesurement
_, predicted = torch.max(outputs.data, 1)
labels = torch.argmax(labels, dim=1) ### line to add for Mixup and Cutmix
accuracy = (predicted == labels).sum() / labels.size(0)
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test'
command
'dlojz.py -b 512 --image-size 176 --test'
Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.
Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task Submitted batch job 248278 jobid = ['248278']
Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.
Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.
#jobid = ['1910208']
display_slurm_queue(name)
JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON)
248278 gpu_p13 pseudo cfor132 R 1:09 1 r6i3n0
Done!
controle_technique(jobid)
Train throughput: 924.57 images/second GPU throughput: 1792.35 images/second epoch time: 1386.09 seconds training time estimation for 90 epochs (with validations): 36.76 hours ----------- training step time average (fwd/bkwd on GPU): 0.285658 sec (8.2%/94.4%) +/- 0.045335 loading step time average (CPU to GPU): 0.268114 sec +/- 0.032765 ----------- ELIGIBLE to run 28 epochs
turbo_profiler(jobid)
>>> Turbo Profiler >>> Training complete in 46.607843 s
TODO : dans le script dlojz.py :
MixUp dans la boucle d'apprentissage après avoir envoyé le batch d'images et de labels au GPU.# distribution of images and labels to all GPUs
#images, labels = mixup_data(images, labels, num_classes=1000, alpha=2.) ## ligne déplacée
images = images.to(gpu, non_blocking=args.non_blocking)
labels = labels.to(gpu, non_blocking=args.non_blocking)
images, labels = mixup_data(images, labels, num_classes=1000, alpha=2., device=gpu)
TODO : dans le script mixup/mixup.py :
device=device à chaque fois que l'on crée un nouveau Tensor pour qu'il soit stocké en mémoire au bon emplacement (CPU ou GPU).Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.
Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task Submitted batch job 248300 jobid = ['248300']
Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.
Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.
#jobid = ['1910460']
display_slurm_queue(name)
JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON)
248300 gpu_p13 pseudo cfor132 R 0:57 1 r6i3n0
Done!
controle_technique(jobid)
Train throughput: 1661.86 images/second GPU throughput: 1788.91 images/second epoch time: 771.15 seconds training time estimation for 90 epochs (with validations): 21.76 hours ----------- training step time average (fwd/bkwd on GPU): 0.286208 sec (7.7%/94.9%) +/- 0.030817 loading step time average (CPU to GPU): 0.021881 sec +/- 0.009838 ----------- ELIGIBLE to run 41 epochs
turbo_profiler(jobid)
>>> Turbo Profiler >>> Training complete in 35.704246 s

Le but de ce TP est d'ajouter la transformation CutMix dans la liste des transformations pour la Data Augmentation et de mesurer grâce au profiler ce que cela implique pour le DataLoader.
La transformation CutMix n'est pas disponible dans torchvision, le script est disponible dans le répertoire cutmix/. On notera que cette transformation impacte à la fois l'image et le label.
On choisira, comme cela est fait habituellement, de mixer 2 images présentes dans le batch généré par le dataloader. Donc cette transformation sera faite dans la boucle d'apprentissage après génération du batch et donc après toutes autres transformations liées à la Data Augmentation.
Dans le script cutmix.py, la variable lambda (lam) correspond à la proportion de la première image par rapport à la deuxième image. Elle est choisie aléatoirement suivant une distribution uniforme définie sur [0, 1].
import os
import torchvision
import torchvision.transforms as transforms
import torchvision.models as models
import torch
import numpy as np
import matplotlib.pyplot as plt
transform = transforms.Compose([
transforms.RandomResizedCrop(176), # Random resize - Data Augmentation
transforms.RandomHorizontalFlip(), # Horizontal Flip - Data Augmentation
transforms.ToTensor() # convert the PIL Image to a tensor
])
train_dataset = torchvision.datasets.ImageNet(root=os.environ['ALL_CCFRSCRATCH']+'/imagenet',
transform=transform)
train_dataset
Dataset ImageNet
Number of datapoints: 1281167
Root location: /gpfsscratch/idris/for/commun/imagenet
Split: train
StandardTransform
Transform: Compose(
RandomResizedCrop(size=(176, 176), scale=(0.08, 1.0), ratio=(0.75, 1.3333), interpolation=bilinear)
RandomHorizontalFlip(p=0.5)
ToTensor()
)
from cutmix.cutmix import cutmix_data
%%time
train_loader = torch.utils.data.DataLoader(dataset=train_dataset,
batch_size=16,
shuffle=True)
batch = next(iter(train_loader))
print('X train batch, shape: {}, data type: {}, Memory usage: {} bytes'
.format(batch[0].shape, batch[0].dtype, batch[0].element_size()*batch[0].nelement()))
print('Y train batch, shape: {}, data type: {}, Memory usage: {} bytes'
.format(batch[1].shape, batch[1].dtype, batch[1].element_size()*batch[1].nelement()))
imgs, targets = batch
imgs, targets = cutmix_data(imgs, targets, num_classes=1000)
for i in range(4):
img = imgs[i].numpy().transpose((1,2,0))
plt.imshow(img)
plt.axis('off')
plt.show()
print(f'target : {torch.max(targets, dim=1)[1][i]}, lambda : {torch.max(targets, dim=1)[0][i]}')
X train batch, shape: torch.Size([16, 3, 176, 176]), data type: torch.float32, Memory usage: 5947392 bytes Y train batch, shape: torch.Size([16]), data type: torch.int64, Memory usage: 128 bytes
target : 433, lambda : 0.9442148804664612
target : 400, lambda : 1.0
target : 718, lambda : 0.5805785059928894
target : 737, lambda : 1.0 CPU times: user 6.92 s, sys: 225 ms, total: 7.15 s Wall time: 7.55 s
TODO : dans le script dlojz.py :
CutMixfrom cutmix.cutmix import cutmix_data
CutMix dans la boucle d'apprentissage après avoir envoyé le batch d'images et de labels au GPU.# distribution of images and labels to all GPUs
images = images.to(gpu, non_blocking=args.non_blocking)
labels = labels.to(gpu, non_blocking=args.non_blocking)
images, labels = cutmix_data(images, labels, num_classes=1000, device=gpu)
Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.
Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task Submitted batch job 248327 jobid = ['248327']
Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.
Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.
#jobid = ['226430']
display_slurm_queue(name)
JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON)
248327 gpu_p13 pseudo cfor132 R 0:55 1 r6i3n0
Done!
controle_technique(jobid)
Train throughput: 1341.21 images/second GPU throughput: 1789.47 images/second epoch time: 955.51 seconds training time estimation for 90 epochs (with validations): 25.98 hours ----------- training step time average (fwd/bkwd on GPU): 0.286119 sec (8.3%/95.3%) +/- 0.047890 loading step time average (CPU to GPU): 0.095627 sec +/- 0.018096 ----------- ELIGIBLE to run 36 epochs
turbo_profiler(jobid)
>>> Turbo Profiler >>> Training complete in 37.928266 s
Le code précédent utilise une boucle for qui empêche de distribuer la transformation sur les cores du GPU. Chaque image dans le batch est traitée de manière séquentielle.
Le but de cette partie est d'optimiser le code de CutMix en générant du calcul matriciel adapté à une parallélisation sur GPU. Il s'agira de manipuler des tenseurs de tailles proportionnelles au batch size et d'utiliser des fonctions d'algèbre linéaire pour aboutir au même résultat numérique tout en accélérant le calcul.
En d'autres termes, au lieu de constituer un masque par image, nous allons directement créer un batch de masques pour tout un batch d'images.
Création d'un batch de masques
Dans un premier temps, pour comprendre la procédure, nous travaillerons avec un batch de 3 images de taille 32x32.
import torch
import numpy as np
import matplotlib.pyplot as plt
batch_size = 3
W = 32
H = 32
En entrée, on connait les coordonnées des coins du cadre à découper pour chaque image du batch (voir illustration ci-dessous).
# coordonnee min dans la largeur pour chaque image du batch
x1 = torch.Tensor([10, 5, 23]).long()
# coordonne max dans la largeur pour chaque image du batch
x2 = torch.Tensor([20, 25, 31]).long()
# coordonnee min dans la hauteur pour chaque image du batch
y1 = torch.Tensor([5, 10, 0]).long()
# coordonne max dans la hauteur pour chaque image du batch
y2 = torch.Tensor([10, 22, 20]).long()

1. Création des vecteurs ligne "largeur" w_int et des vecteurs colonne "hauteur" h_int pour tout le batch d'images
# initialisation à zéro
w_int = torch.zeros(batch_size,1,W) # vecteurs ligne
h_int = torch.zeros(batch_size,H,1) # vecteurs colonne
On initialise les éléments correspondant aux coordonnées minimales (x1 et y1) à 1.
On initialise les éléments correspondant aux coordonnées maximales (x2 et y2) à -1.
Par la suite, les intervalles [x1,x2] et [y1,y2] seront remplis de 1 en demandant à remplir chaque vecteur avec la somme cumulée de ses éléments.
batch_idx = torch.arange(0,batch_size)
# initialisation des indices correspondant aux coord min x1 et y1 à 1
w_int[batch_idx,0,x1] = 1.
h_int[batch_idx,y1,0] = 1.
# initialisation des indices correspondant aux coord max x2 et y2 à -1
w_int[batch_idx,0,x2] = -1.
h_int[batch_idx,y2,0] = -1.
# visualisation des vecteurs ligne "largeur" w_int
for wx in w_int:
plt.imshow(wx)
plt.clim(-1,1)
plt.colorbar(ticks=np.arange(-1,2))
plt.show()
# visualisation des vecteurs colonne "hauteur"
for hx in h_int:
plt.imshow(hx)
plt.clim(-1,1)
plt.colorbar(ticks=np.arange(-1,2))
plt.show()
Pour créer nos vecteurs w_int et h_int, on remplit chaque intervalle [x1,x2] et [y1,y2] de 1 en utilisant la fonction torch.cumsum pour cumuler les valeurs des éléments des vecteurs.
# torch.cumsum(input, dim, *, dtype=None, out=None) → Tensor
# Returns the cumulative sum of elements of input in the dimension dim.
# Parameters
# input (Tensor) – the input tensor.
# dim (int) – the dimension to do the operation over
w_int = torch.cumsum(w_int, dim=2) # vecteurs ligne
h_int = torch.cumsum(h_int, dim=1) # vecteurs colonne
# visualisation des vecteurs masques "largeur"
for wx in w_int:
plt.imshow(wx)
plt.clim(-1,1)
plt.colorbar(ticks=np.arange(-1,2))
plt.show()
# visualisation des vecteurs masques "hauteur"
for hx in h_int:
plt.imshow(hx)
plt.clim(-1,1)
plt.colorbar(ticks=np.arange(-1,2))
plt.show()
2. Créations du batch de masques intérieurs et extérieurs
# multiplication des vecteurs colonne "hauteur" h_int par les vecteurs ligne "largeur" w_int
mask_int = h_int*w_int
# visualisation des masques intérieurs pour chaque image du batch
for m in mask_int:
plt.imshow(m)
plt.clim(-1,1)
plt.colorbar(ticks=np.arange(-1,2))
plt.show()
# les masques extérieurs sont les complémentaires des masques intérieurs
mask_ext = mask_int * (-1) + 1
# visualisation des masques extérieurs
for m in mask_ext:
plt.imshow(m)
plt.clim(-1,1)
plt.colorbar(ticks=np.arange(-1,2))
plt.show()
Implémentation de la fonction de création d'un batch de masques
Maintenant, l'idée est d'implémenter ce qui a été fait dans les cellules précédentes dans une fontion générique, en ajoutant un choix sur le device d'exécution.
TODO : implémenter la fonction de création des masques dans la cellule suivante. Les entrées de la fonction sont :
x1, x2, y1, y2,batch_size,W de l'image,H de l'image,device de calcul.Important : Pour les images RGB (channel de 3), il faut rajouter une dimension en deuxième position dans les masques finaux :
# rajouter une dimension en 2e position pour pouvoir traiter des images RGB
mask_int = mask_int.unsqueeze(1)
mask_ext = mask_ext.unsqueeze(1)
Attention : Ne pas oublier le paramètre device=device à chaque création d'un nouveau Tensor. Par exemple pour :
w_int = torch.zeros(batch_size,1,W,device=device)
def cut_mask(x1, x2, y1, y2, batch_size, W, H, device=None):
mask_ext, mask_int = None, None
### TODO
# initialisation à zéro
w_int = torch.zeros(batch_size,1,W,device=device) # vecteurs ligne
h_int = torch.zeros(batch_size,H,1,device=device) # vecteurs colonne
batch_idx = torch.arange(0,batch_size,device=device)
# initialisation des indices correspondant aux coord min x1 et y1 à 1
w_int[batch_idx,0,x1] = 1.
h_int[batch_idx,y1,0] = 1.
# initialisation des indices correspondant aux coord max x2 et y2 à -1
w_int[batch_idx,0,x2] = -1.
h_int[batch_idx,y2,0] = -1.
w_int = torch.cumsum(w_int, dim=2) # vecteurs ligne
h_int = torch.cumsum(h_int, dim=1) # vecteurs colonne
# multiplication des vecteurs colonne "hauteur" h_int par les vecteurs ligne "largeur" w_int
mask_int = h_int*w_int
# les masques extérieurs sont les complémentaires des masques intérieurs
mask_ext = mask_int * (-1) + 1
# rajouter une dimension pour les images RGB
mask_int = mask_int.unsqueeze(1)
mask_ext = mask_ext.unsqueeze(1)
return mask_ext, mask_int
%%time
train_loader = torch.utils.data.DataLoader(dataset=train_dataset,
batch_size=16,
shuffle=True)
batch = next(iter(train_loader))
print('X train batch, shape: {}, data type: {}, Memory usage: {} bytes'
.format(batch[0].shape, batch[0].dtype, batch[0].element_size()*batch[0].nelement()))
print('Y train batch, shape: {}, data type: {}, Memory usage: {} bytes'
.format(batch[1].shape, batch[1].dtype, batch[1].element_size()*batch[1].nelement()))
imgs, targets = batch
X train batch, shape: torch.Size([16, 3, 176, 176]), data type: torch.float32, Memory usage: 5947392 bytes Y train batch, shape: torch.Size([16]), data type: torch.int64, Memory usage: 128 bytes CPU times: user 4.25 s, sys: 20.7 ms, total: 4.27 s Wall time: 4.32 s
batch_size = 16
W = 176
H = 176
lam = torch.rand(batch_size)
s_index = torch.randperm(batch_size) # Shuffle index
rand_x = torch.randint(W, (batch_size,))
rand_y = torch.randint(H, (batch_size,))
cut_rat = torch.sqrt(1. - lam) ## cut ratio according to the random lambda
x1 = torch.clip(rand_x - rand_x / 2, min=0).long()
x2 = torch.clip(rand_x + rand_x / 2, max=W-1).long()
y1 = torch.clip(rand_y - rand_y / 2, min=0).long()
y2 = torch.clip(rand_y + rand_y / 2, max=H-1).long()
mask_ext, mask_int = cut_mask(x1, x2, y1, y2, batch_size, W, H)
# vérifier si le masque et l'image ont le même nombre de dimensions
try:
assert imgs.dim() == mask_int.dim()
print('OK!')
except:
print(f'Mismatch: \n dim imgs = {imgs.dim()} \n dim mask = {mask_int.dim()} ')
OK!
imgs = mask_ext * imgs + mask_int * imgs[s_index, :]
for i in range(4):
img = imgs[i].numpy().transpose((1,2,0))
plt.imshow(img)
plt.axis('off')
plt.show()
Puis si le résultat est satisfaisant, intégrer la fonction dans le code cutmix/cutmix.py.
TODO : dans le script cutmix/cutmix.py, ajouter la fonction cut_mask définie dans la cellule plus haut.
Soumission du job. Attention vous sollicitez les noeuds de calcul à ce moment-là.
Pour soumettre le job, veuillez basculer la cellule suivante du mode Raw NBConvert au mode Code.
command = f'dlojz.py -b {bs_optim} --image-size {image_size} --test'
n_gpu = 1
jobid = gpu_jobs_submitter(command, n_gpu, MODULE, name=name,
account=account, time_max='00:10:00', constraint='v100-32g')
print(f'jobid = {jobid}')
batch job 0: 1 GPUs distributed on 1 nodes with 1 tasks / 1 gpus per node and 10 cpus per task Submitted batch job 248337 jobid = ['248337']
Copier-coller la sortie jobid = ['xxxxx'] dans la cellule suivante.
Puis, rebasculer la cellule précédente en mode Raw NBConvert, afin d'éviter de relancer un job par erreur.
#jobid = ['256363']
display_slurm_queue(name)
JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON)
248337 gpu_p13 pseudo cfor132 R 0:56 1 r6i3n0
Done!
controle_technique(jobid)
Train throughput: 1669.66 images/second GPU throughput: 1790.34 images/second epoch time: 767.54 seconds training time estimation for 90 epochs (with validations): 21.75 hours ----------- training step time average (fwd/bkwd on GPU): 0.285979 sec (6.9%/96.2%) +/- 0.030070 loading step time average (CPU to GPU): 0.020670 sec +/- 0.006818 ----------- ELIGIBLE to run 41 epochs
turbo_profiler(jobid)
>>> Turbo Profiler >>> Training complete in 35.818331 s